<?php
/**
 * This file is part of the UXLabs project.
 *
 * (c) UXLabs project <http://code.uxlabs.me>
 *
 */

namespace uxl\app\api\controllers;

use uxl\app\api\components\Parameters;
use uxl\app\api\components\RequestFormat;
use uxl\app\api\components\ResultResponse;
use uxl\app\api\components\ResultStatus;
use uxl\app\api\models\AccessLog;
use Yii;
use yii\base\Module;
use yii\web\Controller;

/**
 * 控制器基类
 *
 * @package uxl\app\api\controllers
 * @author ice deng <ice.deng@qq.com>
 */
abstract class BaseController extends Controller
{
    /**
     * 授权请求类型
     */
    const REQUEST_TYPE_AUTH = 'access_auth';
    /**
     * 调用接口方法请求类型
     */
    const REQUEST_TYPE_METHOD = 'api_method';
    /**
     * @var Parameters 请求参数
     */
    protected $parameters;
    /**
     * @var AccessLog 访问记录
     */
    protected $accessLog;
    /**
     * @var string 访问加密密钥
     */
    protected $secretKey;
    /**
     * @var ResultResponse 处理结果
     */
    protected $response;

    /**
     * BaseController constructor.
     * @param string $id
     * @param Module $module
     * @param Parameters $parameters
     * @param array $config
     */
    public function __construct($id, $module, Parameters $parameters, array $config = [])
    {
        parent::__construct($id, $module, $config);

        $this->parameters = $parameters;
        $this->response = new ResultResponse();
        $this->response->format = $this->parameters->format;

        $this->onConstruct();
    }

    /**
     * 实例化后执行
     */
    protected function onConstruct()
    {

    }

    /**
     * 获取请求类型
     * @return string
     */
    abstract protected function getRequestType();

    /**
     * 校验accessKey并设置变量$secretKey的值
     * @return bool
     */
    abstract protected function checkAccessKey();

    /**
     * 获取接口名称
     * @return string
     */
    protected function getApiName()
    {
        return ($this->module ? $this->module->id . '/' : '') . $this->action->getUniqueId();
    }

    /**
     * 记录错误日志
     * @param $errorMsg
     */
    protected function errorLog($errorMsg){
        Yii::error($errorMsg . ', parameters: ' . $this->parameters->toJson(), 'appApi');
    }

    /**
     * @inheritdoc
     */
    public function beforeAction($action)
    {
        if (parent::beforeAction($action) == false) {
            return false;
        }
        // 验证参数完整性
        if ($this->checkParameters() == false) {
            return false;
        }
        // 验证访问密钥
        if ($this->checkAccessKey() == false) {
            return false;
        }
        // 重复访问验证
        if ($this->checkRepeatRequest() == false) {
            return false;
        }
        // 验证签名
        if ($this->verifySign() == false) {
            return false;
        }
        $this->saveRequestLog();
    }

    /**
     * @inheritdoc
     */
    public function afterAction($action, $result)
    {
        $res = parent::afterAction($action, $result);
        $this->saveResponseLog($res);
        return $res;
    }

    /**
     * 保存请求日志
     */
    protected function saveRequestLog()
    {
        /** @var AccessLog $model */
        $model = Yii::$container->get(AccessLog::className());
        $this->accessLog = $model->request($this->getRequestType(), $this->parameters, $this->getApiName());
    }

    /**
     * 保存处理结果到日志
     * @param $result
     */
    protected function saveResponseLog($result)
    {
        if ($this->accessLog == null) {
            return;
        }
        if ($result instanceof ResultResponse) {
            /** @var AccessLog $model */
            $model = Yii::$container->get(AccessLog::className());
            $model->response($this->accessLog->id, $$result->status, $result->content);
        }
    }

    /**
     * 获取签名
     * @param $params
     * @return string
     */
    protected function createSign($params)
    {
        ksort($params);
        $data = [];
        foreach ($params as $k => $v) {
            if (empty($v) && in_array($k, ['sign', 'sign_type']) == false) {
                $data[] = "$k=$v";
            }
        }
        $signStr = implode('&', $data);
        unset($k, $v, $data);
        return strtoupper(md5($signStr));
    }

    /**
     * 设置错误处理结果
     * @param $status
     * @param null $message
     * @param bool $echo
     * @return ResultResponse
     */
    protected function setErrorResponse($status, $message = null, $echo = false)
    {
        $format = $this->parameters->format;
        $format = !!$format ? $format : RequestFormat::JSON;
        $this->response->setError($status, $message, $this->parameters->attach, $format);
        if ($echo) {
            echo $this->response->content;
        } else {
            return $this->response;
        }
    }

    /**
     * 设置处理结果
     * @param $result
     * @param $echo
     * @return ResultResponse
     */
    protected function setResultResponse($result, $echo = false)
    {
        $format = $this->parameters->format;
        $format = !!$format ? $format : RequestFormat::JSON;
        $status = ResultStatus::SUCCESS;
        $error = '';
        $attach = $this->parameters->attach;
        $timestamp = time();
        $resultStr = '';
        switch (strtoupper($this->parameters->format)) {
            case RequestFormat::JSON:
                $resultStr = $this->response->toJson($result);
                break;
            case RequestFormat::XML:
                $resultStr = $this->response->toXml($result);
                break;
            case RequestFormat::JSONP:
                $resultStr = $this->response->toJson($result);
                break;
        }
        $params = [
            'secret_key' => $this->secretKey,
            'status' => $status,
            'result' => $resultStr,
            'timestamp' => $timestamp,
            'error' => $error,
            'attach' => $attach
        ];
        // 生成签名
        $sign = $this->createSign($params);
        $this->response->setResult($status, $resultStr, $error, $attach, $timestamp, $sign, $format);
        if ($echo) {
            echo $this->response->content;
        } else {
            return $this->response;
        }
    }

    /**
     * 验证参数完整性
     * @return bool
     */
    protected function checkParameters()
    {
        $success = true;
        $errorMsg = '';
        // 访问所需的密钥
        if (empty($this->parameters->accessKey)) {
            $success = false;
            $errorMsg = '访问参数密钥(access_key)必须！';
        }
        if (empty($this->parameters->timestamp)) {
            $success = false;
            $errorMsg = '访问参数时间戳(timestamp)必须！';
        }
        if (empty($this->parameters->once)) {
            $success = false;
            $errorMsg = '访问参数随机字符串(once)必须！';
        }
        if (empty($this->parameters->sign)) {
            $success = false;
            $errorMsg = '访问参数随机字符串(sign)必须！';
        }
        if ($success == false) {
            $this->setErrorResponse(ResultStatus::REQUEST_FORMAT_ERROR, $errorMsg, true);
            $this->errorLog('请求参数完整性验证异常：' . $errorMsg);
        }
        return $success;
    }

    /**
     * 是否重复请求
     * @return bool
     */
    protected function checkRepeatRequest()
    {
        /** @var AccessLog $model */
        $model = Yii::$container->get(AccessLog::className());
        $success = $model->repeatRequest($this->getRequestType(), $this->parameters->accessKey, $this->parameters->timestamp, $this->parameters->once);
        if ($success == false) {
            $errorMsg = '重复提交请求';
            $this->setErrorResponse(ResultStatus::REQUEST_REPEAT, $errorMsg, true);
            $this->errorLog($errorMsg);
        }
        return $success == false;
    }

    /**
     * 签名验证
     * @return bool
     */
    protected function verifySign()
    {
        $success = true;

        $params = $this->parameters->toArray();
        $params['secret_key'] = $this->secretKey;
        $sign = $this->createSign($params);
        if ($sign != strtoupper($this->parameters->sign)) {
            $success = false;
            $errorMsg = '签名错误';
            $this->setErrorResponse(ResultStatus::SIGN_ERROR, $errorMsg, true);
            $this->errorLog($errorMsg);
        }
        return $success;
    }
}